Popular Searches
Popular Course Categories
Popular Courses

Implementing Loading Indicators and Error Handling

Implementing Loading Indicators and Error Handling

API Integration and Network Requests

Implementing Loading Indicators and Error Handling in React Native

Course: React Native Training

Topic: Implementing Loading Indicators and Error Handling

Course Link: React Native Training at JustAcademy

Demo Registration: Register for a Free Course Demo

JustAcademy Website: Visit JustAcademy


1. Introduction to Loading Indicators and Error Handling

Modern mobile applications frequently communicate with APIs, databases, authentication services, and other remote systems. These operations take time and may fail because of network connectivity, server problems, invalid input, expired authentication, or unexpected response formats.

In React Native, loading indicators and error handling help developers create applications that remain understandable and usable while asynchronous operations are running or when something goes wrong.

A loading indicator tells users that an operation is in progress. Error handling detects failures, manages application state, and displays meaningful messages or recovery options.

For example, when a user opens a profile screen, the application may need to request profile information from a server. Instead of displaying a blank screen, the app can show a spinner and the message "Loading profile...". If the request fails, the app can display an error message and a Retry button.

1.1 Why Loading Indicators Are Important

  • They communicate that the application is processing a request.
  • They reduce confusion when an operation takes several seconds.
  • They provide feedback during login, registration, uploads, downloads, and API requests.
  • They help prevent users from repeatedly pressing buttons because nothing appears to happen.
  • They improve the perceived responsiveness of the application.

1.2 Why Error Handling Is Important

  • It prevents unexpected failures from producing confusing user experiences.
  • It helps users understand whether an operation succeeded or failed.
  • It provides recovery options such as retrying a request or correcting input.
  • It prevents loading indicators from continuing indefinitely after a request fails.
  • It helps developers diagnose problems through appropriate logging and monitoring.

2. Learning Objectives

After completing this topic, learners should be able to:

  • Understand asynchronous operations in React Native.
  • Use the ActivityIndicator component to display loading feedback.
  • Manage loading, success, and error states using React hooks.
  • Handle API requests using Fetch API and async/await.
  • Identify HTTP errors and network failures.
  • Use try, catch, and finally correctly.
  • Implement retry functionality and prevent duplicate submissions.
  • Handle empty responses, malformed data, and unexpected exceptions.
  • Build reusable patterns for loading and error handling.
  • Test loading and error states in different scenarios.

3. Understanding Asynchronous Operations

An asynchronous operation is a task that can take time to complete without requiring the application to wait synchronously for its result. API requests, authentication, file uploads, and database operations are common examples.

JavaScript commonly handles these operations using Promises and async/await. React Native applications can use the Fetch API to communicate with remote servers.

3.1 Example of an API Request

const response = await fetch(
  'https://jsonplaceholder.typicode.com/users/1'
);
const user = await response.json();
console.log(user);

The request does not necessarily finish immediately. During that time, the application should maintain a loading state and display appropriate feedback.

3.2 The Three Main UI States

State Meaning Typical UI
Loading The operation is still in progress. Spinner, progress message, or skeleton placeholder.
Success The operation completed successfully. Fetched data, confirmation message, or updated screen.
Error The operation failed. Readable error message and an appropriate recovery action.

These states form the foundation of a reliable data-fetching interface. Depending on the screen, an application may also need an initial, empty, refreshing, or partially successful state.

4. Introduction to ActivityIndicator

ActivityIndicator is a built-in React Native component that displays a circular loading indicator. It is commonly used while the application waits for an API response or performs another asynchronous task.

4.1 Importing ActivityIndicator

import {
  ActivityIndicator,
  View,
  Text
} from 'react-native';

4.2 Basic ActivityIndicator Example

import React from 'react';
import {
  ActivityIndicator,
  View,
  Text
} from 'react-native';

export default function LoadingExample() {
  return (
    <View>
      <ActivityIndicator size="large" color="#2563EB" />
      <Text>Loading, please wait...</Text>
    </View>
  );
}

This example displays a spinner and a message. In a real application, the spinner should generally appear conditionally when an operation is actually running.

4.3 Important ActivityIndicator Props

Prop Description Example
size Controls the indicator size. Common values are small and large. size="large"
color Sets the foreground color of the indicator. color="#2563EB"
animating Determines whether the indicator is animating. animating={true}
hidesWhenStopped On iOS, controls whether the indicator hides when it is stopped. hidesWhenStopped={true}

Note: numeric sizes are also supported on Android. For portable examples, use the standard small or large values.

5. Managing Loading State with useState

The useState hook allows a functional component to store and update state. A Boolean loading variable is a straightforward way to control whether a spinner or loading message appears.

5.1 Basic Loading State

import React, { useState } from 'react';
import {
  ActivityIndicator,
  Button,
  Text,
  View
} from 'react-native';

export default function LoadingStateExample() {
  const [isLoading, setIsLoading] = useState(false);

  const startLoading = async () => {
    setIsLoading(true);

    try {
      await new Promise(resolve => setTimeout(resolve, 2000));
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <View>
      {isLoading ? (
        <ActivityIndicator size="large" />
      ) : (
        <Text>Ready to start</Text>
      )}

      <Button
        title="Start Operation"
        onPress={startLoading}
        disabled={isLoading}
      />
    </View>
  );
}

This example simulates a two-second operation. The button becomes disabled during loading, and the indicator disappears when the operation finishes.

5.2 How the State Changes

  1. The user presses the button.
  2. setIsLoading(true) marks the operation as in progress.
  3. React renders the loading indicator.
  4. The asynchronous operation completes or fails.
  5. The finally block sets loading to false.
  6. The screen returns to its normal state.

6. Implementing Loading Indicators for API Requests

A practical loading indicator is usually connected to the lifecycle of an API request. The application should activate it before starting the request and turn it off when the request has completed or failed.

6.1 Basic API Loading Example

import React, { useEffect, useState } from 'react';
import {
  ActivityIndicator,
  Text,
  View
} from 'react-native';

export default function UserScreen() {
  const [isLoading, setIsLoading] = useState(true);
  const [user, setUser] = useState(null);
  const [error, setError] = useState('');

  useEffect(() => {
    const loadUser = async () => {
      try {
        const response = await fetch(
          'https://jsonplaceholder.typicode.com/users/1'
        );

        if (!response.ok) {
          throw new Error(`Request failed: ${response.status}`);
        }

        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError('Unable to load user information.');
      } finally {
        setIsLoading(false);
      }
    };

    loadUser();
  }, []);

  if (isLoading) {
    return (
      <View>
        <ActivityIndicator size="large" />
        <Text>Loading user information...</Text>
      </View>
    );
  }

  if (error) {
    return <Text>{error}</Text>;
  }

  if (!user) {
    return <Text>No user information available.</Text>;
  }

  return (
    <View>
      <Text>Name: {user.name}</Text>
      <Text>Email: {user.email}</Text>
    </View>
  );
}

In this example, the loading indicator appears while the request is in progress. If the request fails, the screen displays an error. If the request succeeds, the user's name and email appear.

6.2 Why finally Is Important

The finally block executes after the try/catch flow completes, whether the request succeeds or an exception is caught. It is a useful place to reset loading state.

try {
  setIsLoading(true);
  const response = await fetch(apiUrl);

  if (!response.ok) {
    throw new Error('Request failed');
  }

  const data = await response.json();
  setData(data);
} catch (error) {
  setError('Something went wrong.');
} finally {
  setIsLoading(false);
}

Without resetting the loading state in all completion paths, an indicator may remain visible indefinitely after an error.

7. Understanding Error Handling in React Native

Error handling is the process of detecting problems, responding safely, and helping users recover. API-related failures can happen at different stages, so developers should distinguish network failures from HTTP errors, parsing errors, and application-level errors.

7.1 Common Types of Errors

Error Type Example Recommended Handling
Network error Device is offline or the server cannot be reached. Show a connection message and offer retry.
HTTP error Server returns 404 or 500. Check response.ok and handle the status.
Authentication error Server returns 401 or 403. Ask the user to sign in again or explain access restrictions.
Validation error Required form fields are missing. Explain which input needs correction.
Parsing error Response body is not valid JSON. Handle parsing failure without crashing the screen.
Unexpected application error Code receives data in an unexpected format. Use safe fallbacks and diagnostic logging.
Timeout or slow request Server takes too long to respond. Consider cancellation, timeout logic, or a retry option.

8. Using try, catch, and finally

JavaScript provides try, catch, and finally to manage exceptions in asynchronous code.

  • try: Contains operations that may fail.
  • catch: Handles an exception thrown in the try block.
  • finally: Runs after the try/catch flow and is useful for cleanup.

8.1 Example

const loadProducts = async () => {
  setIsLoading(true);
  setError('');

  try {
    const response = await fetch(
      'https://example.com/api/products'
    );

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const products = await response.json();
    setProducts(products);
  } catch (error) {
    setError('Unable to load products. Please try again.');
  } finally {
    setIsLoading(false);
  }
};

Replace the example URL with the actual API endpoint used by your application. Avoid displaying raw technical exceptions directly to end users.

9. Understanding Fetch API Error Behavior

An important detail is that Fetch generally rejects its Promise for network-level failures, but it does not automatically throw an exception for HTTP responses such as 404 or 500.

For this reason, developers should check response.ok or inspect response.status before treating the request as successful.

9.1 Incorrect Approach

const response = await fetch(apiUrl);
const data = await response.json();

// A 404 or 500 response may still reach this line.

9.2 Correct Approach

const response = await fetch(apiUrl);

if (!response.ok) {
  throw new Error(`HTTP error: ${response.status}`);
}

const data = await response.json();

This check ensures that an unsuccessful HTTP status is handled by the application's error-handling logic.

10. Displaying User-Friendly Error Messages

Error messages should explain the problem in language that users understand and, where possible, tell them what they can do next.

Technical Situation Suggested Message
Network unavailable Unable to connect. Check your internet connection and try again.
Server unavailable The service is temporarily unavailable. Please try again later.
Invalid login credentials The email or password is incorrect.
Expired session Your session has expired. Please sign in again.
Invalid form input Please check the highlighted fields and try again.
Empty search results No results found. Try another search.

10.1 Store Error Messages in State

const [error, setError] = useState('');

try {
  const response = await fetch(apiUrl);

  if (!response.ok) {
    throw new Error('Request failed');
  }

  const data = await response.json();
  setData(data);
  setError('');
} catch (err) {
  setError('Unable to retrieve information.');
}

10.2 Render an Error Message

{error ? (
  <Text accessibilityRole="alert">
    {error}
  </Text>
) : null}

For sensitive operations, do not reveal server stack traces, private identifiers, access tokens, or internal implementation details in user-facing messages.

11. Complete Example: Loading, Success, Error, and Retry

The following example combines an ActivityIndicator, API request, error message, retry button, and successful data display. It uses a public sample API for demonstration.

import React, { useCallback, useEffect, useState } from 'react';
import {
  ActivityIndicator,
  Button,
  FlatList,
  Text,
  View
} from 'react-native';

const API_URL = 'https://jsonplaceholder.typicode.com/posts';

export default function PostsScreen() {
  const [posts, setPosts] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState('');

  const fetchPosts = useCallback(async () => {
    setIsLoading(true);
    setError('');

    try {
      const response = await fetch(API_URL);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      const result = await response.json();

      if (!Array.isArray(result)) {
        throw new Error('Unexpected response format');
      }

      setPosts(result);
    } catch (err) {
      setError('Could not load posts. Check your connection and retry.');
    } finally {
      setIsLoading(false);
    }
  }, []);

  useEffect(() => {
    fetchPosts();
  }, [fetchPosts]);

  if (isLoading) {
    return (
      <View>
        <ActivityIndicator size="large" color="#2563EB" />
        <Text>Loading posts...</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View>
        <Text accessibilityRole="alert">{error}</Text>
        <Button title="Try Again" onPress={fetchPosts} />
      </View>
    );
  }

  if (posts.length === 0) {
    return <Text>No posts are available.</Text>;
  }

  return (
    <FlatList
      data={posts}
      keyExtractor={item => String(item.id)}
      renderItem={({ item }) => (
        <View>
          <Text>{item.title}</Text>
          <Text>{item.body}</Text>
        </View>
      )}
      refreshing={isLoading}
      onRefresh={fetchPosts}
    />
  );
}

11.1 How This Example Works

  1. The screen starts with a loading indicator visible.
  2. The fetchPosts function requests the list of posts.
  3. The response status is checked before parsing the data.
  4. The application verifies that the returned data is an array.
  5. If successful, the posts are displayed in a FlatList.
  6. If unsuccessful, the application displays an error and Retry button.
  7. The finally block turns off loading after the request finishes.

For a production app, consider whether the full-screen loading state should also be used during refreshes. Often it is better to keep existing content visible and show a smaller refresh indicator.

12. Implementing Loading Indicators in Forms

Forms such as login, registration, password reset, and contact forms often send data to a server. A loading state provides feedback while the submission is being processed and helps prevent duplicate requests.

12.1 Form Submission Example

import React, { useState } from 'react';
import {
  ActivityIndicator,
  Button,
  Text,
  TextInput,
  View
} from 'react-native';

export default function ContactForm() {
  const [email, setEmail] = useState('');
  const [message, setMessage] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [feedback, setFeedback] = useState('');

  const submitForm = async () => {
    if (!email.trim() || !message.trim()) {
      setFeedback('Please complete all fields.');
      return;
    }

    setIsSubmitting(true);
    setFeedback('');

    try {
      const response = await fetch(
        'https://example.com/api/contact',
        {
          method: 'POST',
          headers: {
            Accept: 'application/json',
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ email, message })
        }
      );

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      setFeedback('Your message was submitted successfully.');
      setEmail('');
      setMessage('');
    } catch (error) {
      setFeedback('Unable to submit your message. Please try again.');
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <View>
      <TextInput
        value={email}
        onChangeText={setEmail}
        placeholder="Email address"
        keyboardType="email-address"
        autoCapitalize="none"
        editable={!isSubmitting}
      />

      <TextInput
        value={message}
        onChangeText={setMessage}
        placeholder="Your message"
        multiline
        editable={!isSubmitting}
      />

      {isSubmitting ? (
        <ActivityIndicator size="small" />
      ) : (
        <Button title="Submit" onPress={submitForm} />
      )}

      {feedback ? <Text>{feedback}</Text> : null}
    </View>
  );
}

The API URL is illustrative and must be replaced with a real endpoint. The example performs basic required-field checks; production forms should also validate email format, handle server-side validation errors, and follow the API's authentication and security requirements.

12.2 Preventing Duplicate Form Submissions

  • Disable the submit button while a request is running.
  • Display a spinner or progress label beside the action.
  • Prevent users from changing critical fields during submission when appropriate.
  • For payment or other sensitive operations, use server-supported idempotency protections where available.
  • Do not rely on a disabled button alone to guarantee that duplicate requests cannot occur.

13. Handling Different HTTP Status Codes

HTTP status codes provide information about the outcome of a request. A well-designed app can use these codes to decide what message to display or what action to take.

Status Code Meaning Possible App Response
200 OK Process the successful response.
201 Created Show a creation confirmation when appropriate.
204 No Content Treat the request as successful without expecting a JSON response body.
400 Bad Request Display validation or request information where available.
401 Unauthorized Prompt reauthentication when appropriate.
403 Forbidden Explain that the user does not have permission.
404 Not Found Explain that the requested resource could not be found.
429 Too Many Requests Respect server rate limits and retry guidance.
500 Internal Server Error Show a temporary service error and offer a suitable retry.
503 Service Unavailable Suggest trying again later.

Actual error behavior depends on the API. Read its documented response format, especially for validation messages, authentication errors, rate limits, and retry instructions.

14. Handling Errors Returned by the Server

Some APIs return structured error details in a JSON response. When available, these details can help the application display a useful message.

const response = await fetch(apiUrl);

if (!response.ok) {
  let message = 'The request could not be completed.';

  try {
    const body = await response.json();

    if (typeof body.message === 'string') {
      message = body.message;
    }
  } catch {
    // The error response may not contain JSON.
  }

  throw new Error(message);
}

const data = await response.json();

This pattern attempts to read a server-provided message when the response is unsuccessful. For a production app, map server errors to safe user-facing messages rather than trusting every returned message as appropriate for display.

15. Managing Multiple Loading States

Some screens perform several independent operations. For example, a dashboard might load profile information, notifications, and recommended items. Using a single Boolean loading variable can make it difficult to determine which operation is still running.

15.1 Separate Loading Variables

const [isProfileLoading, setProfileLoading] = useState(true);
const [isNotificationsLoading, setNotificationsLoading] = useState(true);
const [isRecommendationsLoading, setRecommendationsLoading] = useState(true);

Each variable can control the indicator for its own section. This lets one part of the screen become usable while another section is still loading.

15.2 When to Use a Shared Loading State

  • Use a shared state when one operation controls the whole screen.
  • Use separate states when different sections load independently.
  • Use a counter or request-tracking approach if multiple overlapping operations share one indicator.
  • For complex screens, consider a reducer or a data-fetching library to manage related state transitions.

16. Loading Indicators Versus Skeleton Screens

A spinner is not the only way to communicate loading. Different interfaces benefit from different loading patterns.

Pattern Description Suitable Use
ActivityIndicator A circular animated indicator. Short operations, buttons, and small content areas.
Full-screen loading A prominent indicator while the entire screen waits. Initial loading of essential screen data.
Inline loading An indicator placed next to the content being updated. Refreshing one section or loading additional items.
Skeleton screen Placeholder shapes that resemble the expected layout. Feeds, profile cards, lists, and dashboards.
Progress bar Shows progress when measurable progress is available. Uploads, downloads, and multi-step operations.

Choose the simplest pattern that communicates the operation clearly. Avoid showing several competing loading indicators for the same task.

17. Empty States and Partial Results

An empty result is not necessarily an error. A successful search may return no matches, and a user may legitimately have no notifications or saved items.

17.1 Example of an Empty State

if (!isLoading && !error && items.length === 0) {
  return <Text>No items found.</Text>;
}

In a production screen, make sure the data has actually loaded before checking whether it is empty. Otherwise, the app may briefly display an empty message before the request finishes.

17.2 Recommended State Priority

  1. Show initial loading feedback while required data is being retrieved.
  2. Show a recoverable error if the request fails.
  3. Show an empty state when the request succeeds but returns no items.
  4. Show the data when valid results are available.

Refreshing existing content often requires a slightly different presentation: retain the current data while showing an inline refresh indicator and a non-disruptive error if refreshing fails.

18. Preventing Stale Updates and Handling Request Cancellation

A request can finish after a screen has unmounted or after a newer request has started. Without appropriate safeguards, an older response can overwrite newer data or trigger unnecessary updates.

The Fetch API supports request cancellation through AbortController in supported React Native environments. Check compatibility with the React Native version and runtime you target.

18.1 Example with AbortController

import React, { useEffect, useState } from 'react';
import {
  ActivityIndicator,
  Text,
  View
} from 'react-native';

export default function ProfileScreen() {
  const [profile, setProfile] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
    const controller = new AbortController();

    const loadProfile = async () => {
      try {
        const response = await fetch(
          'https://jsonplaceholder.typicode.com/users/1',
          { signal: controller.signal }
        );

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }

        const data = await response.json();
        setProfile(data);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError('Unable to load the profile.');
        }
      } finally {
        if (!controller.signal.aborted) {
          setIsLoading(false);
        }
      }
    };

    loadProfile();

    return () => {
      controller.abort();
    };
  }, []);

  if (isLoading) {
    return <ActivityIndicator size="large" />;
  }

  if (error) {
    return <Text>{error}</Text>;
  }

  return (
    <View>
      <Text>{profile?.name}</Text>
    </View>
  );
}

Cancellation is useful when a component unmounts or a request is no longer needed. An aborted request should not normally be presented as a user-facing failure. For search screens, also consider tracking request identifiers so older results cannot replace newer search results.

19. Retry Strategies and Network Reliability

A Retry button lets users manually repeat a failed operation. Some applications also retry selected transient failures automatically, but retries should be limited and should respect the API's requirements.

19.1 Manual Retry

<Button
  title="Retry"
  onPress={fetchData}
  disabled={isLoading}
/>

19.2 Best Practices for Retry Logic

  • Allow manual retry for recoverable errors.
  • Clear the previous error when a new attempt begins.
  • Show loading feedback during the new request.
  • Avoid unlimited automatic retries.
  • Use exponential backoff and a maximum attempt count when automatic retries are appropriate.
  • Respect server rate-limit information, including retry guidance when provided.
  • Do not blindly retry non-idempotent operations such as payments or order creation.
  • Do not automatically repeat authentication or validation failures without addressing the underlying problem.

19.3 Simple Exponential Backoff Example

const delay = ms =>
  new Promise(resolve => setTimeout(resolve, ms));

async function fetchWithRetry(url, maxAttempts = 3) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      const response = await fetch(url);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      const lastAttempt = attempt === maxAttempts - 1;

      if (lastAttempt) {
        throw error;
      }

      await delay(500 * (2 ** attempt));
    }
  }
}

This is a simplified educational example. It retries all caught failures, including HTTP errors, which is not always appropriate. Production retry logic should classify errors and retry only suitable transient failures, with cancellation and server rate-limit handling where necessary.

20. Creating a Reusable Loading and Error Pattern

When multiple screens use similar loading logic, developers can extract repeated code into a custom hook. This reduces duplication and encourages consistent state handling.

20.1 Example Custom Hook

import { useCallback, useState } from 'react';

export function useApiRequest() {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState('');

  const execute = useCallback(async (url) => {
    setIsLoading(true);
    setError('');

    try {
      const response = await fetch(url);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      const result = await response.json();
      setData(result);
      return result;
    } catch (err) {
      setError('Unable to complete the request.');
      throw err;
    } finally {
      setIsLoading(false);
    }
  }, []);

  return {
    data,
    isLoading,
    error,
    execute
  };
}

The hook exposes data, loading state, error state, and a function to execute the request. Since it rethrows errors, a calling component must catch them if it invokes execute without handling the rejection elsewhere.

For applications with caching, pagination, background refreshes, request deduplication, or complex synchronization, consider a dedicated data-fetching library instead of building every feature manually.

21. Common Mistakes and Troubleshooting

Problem Possible Cause Solution
Spinner never disappears Loading state is not reset on every completion path. Use finally and inspect early returns.
HTTP 404 is treated as success The response status is not checked. Check response.ok before parsing successful data.
Error message does not appear Error state is not updated or the render condition is incorrect. Verify state changes and conditional rendering.
Data is undefined The API response shape differs from expectations. Inspect the response and validate its structure.
Invalid JSON error The server returned HTML, an empty body, or malformed JSON. Check the endpoint and parse the response according to its documented format.
Repeated requests Effect dependencies or event handlers cause requests to run more often than intended. Review hook dependencies and request triggers.
Multiple submissions The user can trigger the same operation repeatedly. Disable the action during submission and add server-side safeguards where needed.
Old results replace new results Requests finish out of order. Cancel obsolete requests or track request identifiers.
Network request fails on a device Incorrect URL, unavailable server, TLS issue, or device connectivity problem. Check the endpoint, device logs, network access, and HTTPS configuration.
Refresh removes existing data The entire screen is replaced with a full-screen loader. Keep previous content visible during background refreshes where appropriate.

22. Testing Loading Indicators and Error Handling

Testing should cover more than the successful request path. Every important asynchronous screen should be checked under loading, success, empty, and failure conditions.

22.1 Manual Testing Checklist

  • Confirm that the indicator appears when a request begins.
  • Confirm that it disappears after success.
  • Confirm that it disappears after failure.
  • Test a server response with an unsuccessful HTTP status.
  • Test an unavailable endpoint or disconnected network.
  • Verify that users can retry a recoverable request.
  • Check that empty results display an appropriate empty state.
  • Check that repeated button presses do not cause unwanted duplicate operations.
  • Test slow responses and screen navigation while requests are pending.
  • Confirm that technical details and sensitive information are not exposed in error messages.
  • Check accessibility labels and ensure the loading state is understandable to assistive technology users.

22.2 Automated Testing Scenarios

Component tests can mock network responses and verify which UI appears in each state. Typical scenarios include:

  1. Render the component and verify the loading indicator is visible initially.
  2. Resolve the mocked request and verify that the expected data is displayed.
  3. Reject the mocked request and verify that an error message appears.
  4. Simulate a retry and verify that the request is attempted again.
  5. Return an empty list and verify that the empty-state message appears.

23. Performance and User Experience Best Practices

  • Use a full-screen spinner only when the whole screen depends on the pending request.
  • Prefer inline loading indicators when only a small section is updating.
  • Use skeleton placeholders when they make the expected layout clearer.
  • Display concise, friendly error messages with actionable next steps.
  • Keep existing data visible during refreshes when it remains valid.
  • Use appropriate cancellation for requests that are no longer needed.
  • Validate response data before relying on fields in the UI.
  • Prevent repeated actions when a request is already running.
  • Use HTTPS for network communication.
  • Never include passwords, access tokens, or sensitive user data in logs.
  • Use a logging or monitoring service suitable for production diagnostics.
  • Make loading and error states accessible and consistent throughout the application.

24. Mini Project: User Directory with Loading and Error Handling

Project Objective: Build a React Native screen that retrieves users from an API and displays loading, success, empty, and error states.

24.1 Project Requirements

  • Fetch user data from a sample or custom REST API.
  • Display an ActivityIndicator while the initial request is running.
  • Display user names and email addresses when the request succeeds.
  • Show an error message if the request fails.
  • Provide a Retry button.
  • Display a helpful message when no users are returned.
  • Prevent accidental repeated requests where appropriate.

24.2 Suggested Implementation Steps

  1. Create a component named UserDirectoryScreen.
  2. Import useEffect, useState, ActivityIndicator, FlatList, Text, View, and Button.
  3. Create state variables for users, loading, and error.
  4. Write an asynchronous function to fetch user data.
  5. Check the HTTP response status and parse the response body.
  6. Use try/catch/finally to manage success, error, and loading state.
  7. Render the loading indicator, error message, empty state, or user list according to the current state.
  8. Add a Retry button and test both successful and unsuccessful responses.

24.3 Expected Result

Scenario Expected Behavior
Request begins Loading indicator is visible.
Users are returned The user list is displayed.
No users are returned An empty-state message is displayed.
Request fails A friendly error message and Retry option appear.
Retry succeeds The error disappears and the user list is displayed.

25. Practice Exercises

  1. Create a screen that shows an ActivityIndicator for two seconds and then displays a success message.
  2. Fetch a list of products and display them using FlatList.
  3. Implement an error message when an API returns an HTTP 404 status.
  4. Add a Retry button to a screen that failed to fetch data.
  5. Build a login form that disables its submit button while the request is running.
  6. Display an empty-state message when an API returns an empty array.
  7. Implement separate loading indicators for a profile section and a notification section.
  8. Cancel a pending request when the relevant screen unmounts.
  9. Write tests for loading, success, error, and empty states.
  10. Improve an existing screen so that refreshing data does not hide previously loaded content unnecessarily.

26. Interview Questions and Answers

Q1. What is a loading indicator in React Native?

A loading indicator provides visual feedback that an operation is in progress. React Native provides the built-in ActivityIndicator component for circular loading feedback.

Q2. What is ActivityIndicator?

ActivityIndicator is a React Native component that displays an animated circular indicator. Its commonly used props include size, color, and animating.

Q3. Why is loading state necessary?

Loading state lets the interface respond to asynchronous operations. It helps users understand that the application is working and allows the UI to change when the operation completes.

Q4. How can loading state be managed in a functional component?

Use the useState hook to store a Boolean value such as isLoading. Set it to true when the operation starts and reset it when the operation finishes.

Q5. What is error handling?

Error handling detects and responds to failures, such as network problems, unsuccessful HTTP responses, invalid data, or unexpected exceptions.

Q6. What is the purpose of try/catch?

The try block contains operations that may fail, while catch handles exceptions thrown during those operations.

Q7. Why is finally useful for API requests?

The finally block runs after the try/catch flow and is useful for resetting loading state regardless of whether the operation succeeds or fails.

Q8. Does Fetch automatically throw an error for HTTP 404?

No. Fetch typically rejects for network-level failures, but HTTP error statuses such as 404 or 500 still produce a Response object. Developers should check response.ok or response.status.

Q9. How can an error message be displayed?

Store a user-friendly message in state and conditionally render it using a Text component. Provide a recovery action when one is available.

Q10. How do you prevent duplicate form submissions?

Disable the submit action while the request is running and show loading feedback. For sensitive operations, also use appropriate server-side duplicate-request protections.

Q11. What is a retry mechanism?

A retry mechanism lets the application attempt a failed operation again. It can be triggered by the user or automatically for selected transient errors.

Q12. What is AbortController used for?

AbortController can cancel supported Fetch requests that are no longer needed, such as when a component unmounts or a search query changes.

Q13. What is the difference between an error state and an empty state?

An error state means the operation failed. An empty state means the operation succeeded but returned no results. The UI should communicate these situations differently.

Q14. When should a skeleton screen be used?

A skeleton screen can be useful when the expected content layout is known, such as a feed or dashboard. It gives users a preview of the content structure while the data loads.

Q15. How can API errors be tested?

Mock network responses and test unsuccessful HTTP statuses, rejected requests, malformed responses, empty results, and retry behavior. Also test that loading state resets after failure.

27. Quick Revision Checklist

  • Understand asynchronous requests and their lifecycle.
  • Use ActivityIndicator to communicate loading progress.
  • Manage loading, data, and error state explicitly.
  • Use try/catch/finally around asynchronous operations.
  • Check response.ok before processing a successful API response.
  • Validate response formats and handle empty results.
  • Show user-friendly error messages.
  • Offer retry functionality for recoverable failures.
  • Prevent duplicate submissions and stale state updates.
  • Use request cancellation where supported and appropriate.
  • Test loading, success, error, and empty states.
  • Keep sensitive details out of user-facing messages and logs.

28. Conclusion

Implementing loading indicators and error handling is an essential part of developing reliable React Native applications. The ActivityIndicator component communicates progress, while state management controls what users see during asynchronous operations.

Using try/catch/finally, checking HTTP status codes, validating response data, and displaying helpful error messages allows applications to respond more safely when requests fail. Retry actions, request cancellation, accessible feedback, and thorough testing further improve the user experience.

By applying these techniques consistently, developers can build React Native applications that remain clear, responsive, and resilient across both successful and unsuccessful network conditions.

29. Official Learning Resources

30. Course and Demo Registration

Explore the React Native course to learn about mobile application development, components, hooks, API integration, state management, and practical application projects.

React Native Training: View the React Native Course

Register for a Course Demo: Book Your Demo Session

Official JustAcademy Website: https://www.justacademy.co/

whatsapp